Análisis cartera de clientes

RFM Análisis

Este iPython Notebbok explica como realizar un análisis de clientes en base a los datos históricos, La idea es ver cual es el estado de salud de mi cartera de clientes.

In [1]:
#importamos las siguientes librerías
import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from datetime import datetime
import squarify
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
In [2]:
#ruta del archivo con los datos
ruta='C:/Users/alfredo/Documents/Diplomado'
In [3]:
#nombre de las columnas
columns_name=['pk', 'sociedad', 'sector', 'ofvta', 'cod_cliente', 'nombre', 'vendedor', 'clas_fiscal', 
              'pedido', 'material', 'marca', 'linea', 'linea_o', 'id_pedido', 'costo', 'gr_mat', 'fecha',
              'fecha_2', 'ingreso', 'mesn', 'dia', 'mes', 'año', 'funica' ]
In [4]:
df=pd.read_csv(ruta+'/Query1.txt', sep=';', decimal=',', encoding = 'ISO-8859-1', names=columns_name, header=0)
In [5]:
#imprimimos las 5 primeras filas del dataframe
df.head()
Out[5]:
pk sociedad sector ofvta cod_cliente nombre vendedor clas_fiscal pedido material ... costo gr_mat fecha fecha_2 ingreso mesn dia mes año funica
0 1331712 CC07 S2 G04 150230 M.O.P. DIRECCION DE VIALIDAD ST ZF01 CONCEPCION 150897063 ... 91.94 NaN 30/10/2015 2015.10.01 00:00:00 136.54 2015-10-01 30 10 2015 201510
1 1331717 CC07 S2 G92 233597 Maquinarias y Servicios Alianza Lim ST ZF01 IQUIQUE 151185911 ... 0.00 NaN 30/10/2015 2015.10.01 00:00:00 0.00 2015-10-01 30 10 2015 201510
2 1331699 CC07 S2 G04 71296 WTY_VOLVO BRASIL ST ZF01 CONCEPCION 151004930 ... 0.00 NaN 19/10/2015 2015.10.01 00:00:00 0.00 2015-10-01 19 10 2015 201510
3 1331701 CC07 S2 G10 150262 M.O.P. DIRECCION DE VIALIDAD ST ZF01 TALCA 151014216 ... 0.00 NaN 30/10/2015 2015.10.01 00:00:00 0.00 2015-10-01 30 10 2015 201510
4 1331724 CC07 S2 G14 263052 BRANDA SERVICIOS LIMITADA ST ZF01 SERENA 150902447 ... 152.85 NaN 30/10/2015 2015.10.01 00:00:00 188.04 2015-10-01 30 10 2015 201510

5 rows × 24 columns

In [6]:
#verificamos los tipos de datos
df.info()

RangeIndex: 149302 entries, 0 to 149301
Data columns (total 24 columns):
pk             149302 non-null int64
sociedad       149302 non-null object
sector         149302 non-null object
ofvta          149302 non-null object
cod_cliente    149302 non-null object
nombre         149302 non-null object
vendedor       149302 non-null object
clas_fiscal    77062 non-null object
pedido         149295 non-null object
material       149299 non-null object
marca          149302 non-null object
linea          149302 non-null object
linea_o        149302 non-null object
id_pedido      143160 non-null object
costo          149302 non-null float64
gr_mat         144412 non-null object
fecha          149302 non-null object
fecha_2        149302 non-null object
ingreso        149302 non-null float64
mesn           149302 non-null object
dia            149302 non-null int64
mes            149302 non-null int64
año            149302 non-null int64
funica         149302 non-null int64
dtypes: float64(2), int64(5), object(17)
memory usage: 27.3+ MB
In [7]:
#modificamos el formato de las siguientes variables a "category" para  ahorrar un poco más de memoria 
df[['sociedad','sector','ofvta','clas_fiscal','marca','linea','linea_o']]=df[['sociedad','sector','ofvta','clas_fiscal','marca'
                                                                              ,'linea',
                                                                              'linea_o']].apply(lambda x: x.astype('category'))
In [8]:
#transformo la fecha a formato date
df['Fecha_factura']=pd.to_datetime(df['fecha'], format='%d/%m/%Y')
In [9]:
#modifico el formato de los ingresos y costos
df[['ingreso', 'costo']] = df[['ingreso', 'costo']].apply(lambda x: x.astype(np.int64))
In [10]:
#elimino los registros con ingreso 0 en las filas (esto es un error del ERP)
df=df[df.ingreso != 0.0]
In [11]:
#imprimo estadísticos básicos de venta
df[['ingreso']].groupby(lambda x: df['Fecha_factura'][x].year).describe()
Out[11]:
ingreso
count mean std min 25% 50% 75% max
2015 36139.0 330.435513 1803.550249 -12579.0 33.0 89.0 253.0 166287.0
2016 38342.0 328.159851 1288.282513 -11942.0 27.0 88.0 253.0 64582.0

1 Creación tabla RFM

In [12]:
startDate_history = datetime.strptime('20150901', '%Y%m%d').date()
endDate_history=datetime.strptime('20160229', '%Y%m%d').date()
In [13]:
def getRFM(dataFrame, sDate, eDate): 
    """
    Función que calcula la frecuencia, ingreso, la recencia de compra y si utilizó servicio técnico
    """
    #ordeno el dataframe por fecha descendiente
    dataFrame=dataFrame.sort_values(by='Fecha_factura', ascending=False)
    
    #filtro los datos segun la fecha histórica y los almaceno en un df temporal
    tmpdf=dataFrame[(dataFrame.Fecha_factura >= sDate) & (dataFrame.Fecha_factura <= eDate)]
    
    #elimino filas con los ID duplicados, asigno a una nuevo df modifico el indice para que sea los ID de cliente
    newdf=tmpdf.drop_duplicates(['cod_cliente']).set_index(['cod_cliente'])
    
    #calculo la recencia (días), menores días significa más reciente
    newdf['recency']=(eDate-newdf['Fecha_factura']).dt.days
    
    #calculo la frecuencia de compra   
    frecuency=tmpdf.groupby(['cod_cliente'] )['cod_cliente'].count()
    
    #join la variable frecuencia con newdf
    newdf=newdf.join(frecuency)
    
    #renombro las columnas
    newdf=newdf.rename(columns={'cod_cliente': 'frecuency'})
    
    #calculo el ingreso
    monetary=tmpdf.groupby(['cod_cliente'] )[['cod_cliente','ingreso']].sum()    
    newdf=newdf.merge(monetary, left_index=True,right_index=True)
    newdf=newdf.rename(columns={'ingreso_y': 'monetary'})    
      
    
    #returno un nuevo df con la recencia, frecuencia, ingreso y st
    return newdf
In [14]:
#tomamos los datos en un rango de fecha y lo almaceno en la variable historia
historia=getRFM(df, startDate_history, endDate_history)
In [15]:
#solo voy a trabajar con los datos recency, frecuency, monetary y los almaceno en la variable rfmTabla
rfmTabla=historia[['recency', 'frecuency', 'monetary']]
In [16]:
rfmTabla.head(5)
Out[16]:
recency frecuency monetary
cod_cliente
249252 0 17 1407
210289 0 263 39663
232443 0 1388 444922
211617 0 44 5357
213674 0 147 15732

2 Determinando RFM

In [17]:
#ordeno de los montos de mayor a menor
test= pd.DataFrame( rfmTabla['monetary'].sort_values(ascending=False))
In [18]:
#Calculo el porcentaje de cada cliente
test['pc']= 100*test['monetary']/test['monetary'].sum()
In [19]:
#Almaceno el total acumulado en la variable cum_pc
test['cum_pc']=test['pc'].cumsum()
In [20]:
#clasifico por pareto el 50% como 1, entre 50% a 80% como 2 y sobre el 80% 3
test['t_monetary'] = test['cum_pc'].apply(lambda x: 1 if x > 80 else( 3 if x < 50  else 2))
In [21]:
test.head()
Out[21]:
monetary pc cum_pc t_monetary
cod_cliente
229562 581311 11.304984 11.304984 3
232443 444922 8.652574 19.957558 3
232449 224782 4.371424 24.328982 3
ICC03 216256 4.205616 28.534598 3
232508 175988 3.422508 31.957106 3
In [22]:
#corto la recency y la frecuencia según los percentil 33%, 66% y 99,9%
rfmTabla['t_recency']=pd.qcut(rfmTabla['recency'],3, labels=[3,2,1])
rfmTabla['t_frecuency']=pd.qcut(rfmTabla['frecuency'],3, labels=[1,2,3])
In [23]:
rfmTabla=rfmTabla.merge(test,  left_index=True, right_index=True)
In [24]:
rfmTabla.head()
Out[24]:
recency frecuency monetary_x t_recency t_frecuency monetary_y pc cum_pc t_monetary
cod_cliente
249252 0 17 1407 3 3 1407 0.027362 95.262362 1
210289 0 263 39663 3 3 39663 0.771342 60.208997 2
232443 0 1388 444922 3 3 444922 8.652574 19.957558 3
211617 0 44 5357 3 3 5357 0.104180 86.403140 1
213674 0 147 15732 3 3 15732 0.305946 74.581011 2
In [26]:
#concateno los resultados en la variable RFMClase
rfmTabla['RFMClase']=rfmTabla['t_recency'].astype('str')  + rfmTabla['t_frecuency'].astype('str')  + rfmTabla['t_monetary'].astype('str')  
In [27]:
rfmTabla.head(5)
Out[27]:
recency frecuency monetary_x t_recency t_frecuency monetary_y pc cum_pc t_monetary RFMClase
cod_cliente
249252 0 17 1407 3 3 1407 0.027362 95.262362 1 331
210289 0 263 39663 3 3 39663 0.771342 60.208997 2 332
232443 0 1388 444922 3 3 444922 8.652574 19.957558 3 333
211617 0 44 5357 3 3 5357 0.104180 86.403140 1 331
213674 0 147 15732 3 3 15732 0.305946 74.581011 2 332

3 Categorizando RFM

In [28]:
#importo un archivo con la clasificación de todas las categorías
categoria=pd.read_csv(ruta+'/categoria.txt', sep='\t', encoding = 'ISO-8859-1', header=0)
In [29]:
categoria['RFMClase']=categoria['RFMClase'].astype('str')
In [30]:
categoria.info()

RangeIndex: 27 entries, 0 to 26
Data columns (total 2 columns):
RFMClase    27 non-null object
clase       27 non-null object
dtypes: object(2)
memory usage: 512.0+ bytes
In [31]:
rfmTabla.reset_index(level=0, inplace=True)
In [32]:
rfmTabla.head()
Out[32]:
cod_cliente recency frecuency monetary_x t_recency t_frecuency monetary_y pc cum_pc t_monetary RFMClase
0 249252 0 17 1407 3 3 1407 0.027362 95.262362 1 331
1 210289 0 263 39663 3 3 39663 0.771342 60.208997 2 332
2 232443 0 1388 444922 3 3 444922 8.652574 19.957558 3 333
3 211617 0 44 5357 3 3 5357 0.104180 86.403140 1 331
4 213674 0 147 15732 3 3 15732 0.305946 74.581011 2 332
In [33]:
#realizo un join entre las cateorias
test= rfmTabla.merge(categoria,  left_on=['RFMClase'], right_on=['RFMClase'], how='outer')
In [34]:
test.head(5)
Out[34]:
cod_cliente recency frecuency monetary_x t_recency t_frecuency monetary_y pc cum_pc t_monetary RFMClase clase
0 249252 0.0 17.0 1407.0 3 3 1407.0 0.027362 95.262362 1.0 331 Cliente fiel
1 211617 0.0 44.0 5357.0 3 3 5357.0 0.104180 86.403140 1.0 331 Cliente fiel
2 200091 0.0 40.0 10362.0 3 3 10362.0 0.201514 80.853126 1.0 331 Cliente fiel
3 229135 0.0 21.0 4630.0 3 3 4630.0 0.090041 87.268822 1.0 331 Cliente fiel
4 213647 0.0 43.0 4496.0 3 3 4496.0 0.087435 87.622647 1.0 331 Cliente fiel
In [35]:
grupo=test.groupby(['clase']).size().reset_index(name='counts')
grupo
Out[35]:
clase counts
0 Campeón 11
1 Cliente fiel 153
2 Cliente poco fidelizado 343
3 Cliente potencial fiel 100
4 Cliente valioso con riesgo 6
5 Dormido 82
6 No se puede perder 7
7 Perdido 178
In [36]:
grupo['sales_ratio'] = 100*(grupo['counts']/grupo['counts'].sum())
In [37]:
#matplotlib para escalar los datos entre el mínimo, máximo, así le asignamos una escala  a nuestros datos.
norm = matplotlib.colors.Normalize(vmin=min(grupo.counts), vmax=max(grupo.counts))
colors = [matplotlib.cm.Blues(norm(value)) for value in grupo.counts]

#crear un gráfico y redimensionarlo
fig = plt.gcf()
ax = fig.add_subplot()
fig.set_size_inches(16, 8)


labels = ["%s\n%d clientes\n%.2f %% "  % (label) for label in zip(grupo.clase, grupo.counts, grupo.sales_ratio)]

#Usa la librería squarify para graficar nuestros datos, se etiqueta y agrega color
squarify.plot(label=labels ,sizes=grupo.counts, color = colors, alpha=.6)
plt.title("Estado cartera de clientes",fontsize=23,fontweight="bold")

plt.axis('off')
plt.show()

4 Conclusiones

Podemos revisar el estado en que se encuentra nuestra cartera de clientes, para el análisis se utilizaron las siguientes categorias

Campeón – 333: Compra recientemente, con frecuencia y nos deja mucho dinero representa el 50% de todos los ingresos de la compañia.

Cliente fiel – X3X: Compra con regularidad y tiene una aceptable frecuencia, representa el 30% de los ingresos de la compañia.

Cliente potencial fiel X2X: Clientes que tienen cierta regularidad y nos deja buenos ingresos.

Cliente valioso con riesgo 12X: Clientes que hace un tiempo no nos visitan y que regularmente nos dejaban buenos ingresos.

Cliente no se puede perder 113: Clientes que hace un tiempo no nos visitan y que regularmente nos dejaban muchos ingresos (pertenecen al segmento que representa el 50% de los ingresos de la compañia).

Cliente poco fidelizado XX1: Clientes que nos visitan esporádicamente y sus ingresos no son significativos.

Cliente perdido 111: Clientes hace un buen tiempo no nos visitan, su frecuencia es ocasional y sus ingresos mo son significativos.